﻿using System;
using RayDen.Library.Core;
using RayDen.Library.Core.Primitives;
using RayDen.RayEngine.Core.Library;
using RayDen.RayEngine.Core.Types;

namespace RayDen.RayEngine.Data
{
    public class FiniteAppertureCamera : BaseCamera
    {
        float focal_scale = 120.0f;

        public new float Fov
        {
            get { return fieldOfView; }
            set { fieldOfView = value; }
        }

        public float fieldOfView = 45f, lensRadius = 0.9f;
        public float clipHither = 1f, clipYon = 120f;
        Vector dir, x, y;
        private int Width;
        private int Height;
        private FastRandom rnd;

        public FiniteAppertureCamera(Point eye, Vector dirt, Vector up, int width, int height, float fov)
        {
            rnd = new FastRandom();
            this.fieldOfView = fov;
            this.Height = height;
            this.Width = width;
            this.Position = eye;
            this.Target = dirt;
            this.Up = up;

            this.Update();
        }

        public float FieldOfView
        {
            get { return fieldOfView; }
            set { fieldOfView = value; }
        }

        public void Update()
        {
            dir = (Target - (Vector)Position);
            dir = dir.Normalize();

            Vector up = Up;

            float k = MathLab.Radians(fieldOfView);
            x = (dir ^ up);
            x.Normalize();
            x *= Width * k / Height;

            y = (dir ^ x);
            y.Normalize();
            y *= k;
            this.CreateProjectionMatrix();
        }


        private Matrix4x4 projection;
        public void CreateProjectionMatrix()
        {
            var U = x;
            var V = y;
            var N = (Target - (Vector)Position).Normalize();
            var C = Position;
            int w = Width;
            int h = Height;
            var m1 = new Matrix4x4(w / 2.0f * ((float)Math.Tan(MathLab.Radians(fieldOfView * 0.5f))), 0, -w * 0.5f, 0,
                                   0, h / 2.0f * ((float)Math.Tan(MathLab.Radians(fieldOfView * 0.5f))), -h * 0.5f, 0,
                                   0, 0, 1, 0,
                                   0, 0, -1, 0);
            var m2 = new Matrix4x4(U.X, U.Y, U.Z, 0,
                                   V.X, V.Y, V.Z, 0,
                                   -N.X, -N.Y, -N.Z, 0,
                                   0, 0, 0, 1);
            var m3 = new Matrix4x4(1, 0, 0, -C.X,
                                   0, 1, 0, -C.Y,
                                   0, 0, 1, -C.Z,
                                   0, 0, 0, 1);
            this.projection = m1 * m2 * m3;
            projectTransform = new Transform(projection, Matrix4x4.Inverse(ref projection));

        }

        private Transform projectTransform;

        public override Point UnProject(ref Point v)
        {
            return projectTransform & v;
        }
        public void Sample(float xp, float yp, float u0, float u1, float u2, out IRay r, out float pdf)
        {
            Point Pras = new Point(xp, yp, 0);
            //Point Pcamera = UnProject(ref Pras);
            var u = 2.0f * xp / Width - 1.0f;
            var v = 1.0f - 2.0f * yp / Height;

            Vector ray_dir = new Vector(x.x * u + y.x * v + dir.x, x.y * u + y.y * v + dir.y, x.z * u + y.z * v + dir.z);
            Vector ray_target = (Vector)(Position + ray_dir * focal_scale);

            Point ray_origin = Position;

            var ray = new RayData(ray_origin, ray_dir, 0f, 1e20f);
            // Modify ray for depth of field
            if (lensRadius > 0f)
            {
                // Sample point on lens
                float lensU = 0f, lensV = 0f;
                float samplelensU = rnd.NextFloat();
                float samplelensV = rnd.NextFloat();

                MC.ConcentricSampleDisk(samplelensU, samplelensV, ref lensU, ref lensV);
                lensU *= lensRadius;
                lensV *= lensRadius;

                ray_target += lensRadius * (lensU * Vector.Normalize(x) + lensV * Vector.Normalize(y));


                // Compute point on plane of focus
                //float ft = focalDistance / ray.Dir.z;
                //Point Pfocus = ray.Point(ft);

                // Update ray for effect of lens
                ray.Org = ray_origin;
                ray.Dir = Vector.Normalize(ray_target - (Vector)ray_origin);
            }
            pdf = 1;
            r = (IRay)ray;

        }
        public override void GetRay(double xp, double yp, out RayData ray)
        {

            /*
             
             float3 ray_origin = eye;
		    float3 ray_direction = d.x*U + d.y*V + W;
		    float3 ray_target = ray_origin + focal_scale * ray_direction;


		float2 sample = optix::square_to_disk(make_float2(jt.x, jt.y));
		ray_origin = ray_origin + aperture_radius * ( sample.x * normalize( U ) + sample.y * normalize( V ) );
		ray_direction = normalize(ray_target - ray_origin);
             
             */
            // Generate raster and camera samples
            Point Pras = new Point((float)xp, (float)yp, 0);
            Point Pcamera = UnProject(ref Pras);
            var u = (float)(2.0f * xp / Width - 1.0f);
            var v = (float)(1.0f - 2.0f * yp / Height);

            Vector ray_dir = new Vector(x.x * u + y.x * v + dir.x, x.y * u + y.y * v + dir.y, x.z * u + y.z * v + dir.z);
            Vector ray_target = (Vector)(Position + ray_dir * focal_scale);

            Point ray_origin = Position;

            ray = new RayData(ray_origin, ray_dir, 0f, 1e20f);
            // Modify ray for depth of field
            if (lensRadius > 0f)
            {
                // Sample point on lens
                float lensU = 0f, lensV = 0f;
                float samplelensU = rnd.NextFloat();
                float samplelensV = rnd.NextFloat();

                MC.ConcentricSampleDisk(samplelensU, samplelensV, ref lensU, ref lensV);
                lensU *= lensRadius;
                lensV *= lensRadius;

                ray_target += lensRadius * (lensU * Vector.Normalize(x) + lensV * Vector.Normalize(y));


                // Compute point on plane of focus
                //float ft = focalDistance / ray.Dir.z;
                //Point Pfocus = ray.Point(ft);

                // Update ray for effect of lens
                ray.Org = ray_origin;
                ray.Dir = Vector.Normalize(ray_target - (Vector) ray_origin);
            }
            


            //ray->time = Lerp(sample.time, shutterOpen, shutterClose);
            //CameraToWorld(*ray, ray);
            //return 1.f;
        }

        public override void LookAt(Point vector, Point vector_2, Vector vector_3)
        {
            this.Position = vector;
            this.Target = (Vector)vector_2;
            this.Up = vector_3;
            this.Update();
        }
    }
}